// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.Shared.TrimAnalysis;

// This is needed due to NativeAOT which doesn't enable nullable globally yet
#nullable enable

namespace ILLink.Shared
{
    internal static class Annotations
    {
        public static bool SourceHasRequiredAnnotations(
            DynamicallyAccessedMemberTypes sourceMemberTypes,
            DynamicallyAccessedMemberTypes targetMemberTypes,
            out string missingMemberTypesString)
        {
            missingMemberTypesString = string.Empty;

            var missingMemberTypes = GetMissingMemberTypes(targetMemberTypes, sourceMemberTypes);
            if (missingMemberTypes == DynamicallyAccessedMemberTypes.None)
                return true;

            missingMemberTypesString = GetMemberTypesString(missingMemberTypes);
            return false;
        }

        public static DynamicallyAccessedMemberTypes GetMissingMemberTypes(DynamicallyAccessedMemberTypes requiredMemberTypes, DynamicallyAccessedMemberTypes availableMemberTypes)
        {
            if (availableMemberTypes.HasFlag(requiredMemberTypes))
                return DynamicallyAccessedMemberTypes.None;

            if (requiredMemberTypes == DynamicallyAccessedMemberTypes.All)
                return DynamicallyAccessedMemberTypes.All;

            var missingMemberTypes = requiredMemberTypes & ~availableMemberTypes;

            // PublicConstructors is a special case since its value is 3 - so PublicParameterlessConstructor (1) | _PublicConstructor_WithMoreThanOneParameter_ (2)
            // The above bit logic only works for value with single bit set.
            if (requiredMemberTypes.HasFlag(DynamicallyAccessedMemberTypes.PublicConstructors) &&
                !availableMemberTypes.HasFlag(DynamicallyAccessedMemberTypes.PublicConstructors))
                missingMemberTypes |= DynamicallyAccessedMemberTypes.PublicConstructors;

            return missingMemberTypes;
        }

        public static string GetMemberTypesString(DynamicallyAccessedMemberTypes memberTypes)
        {
            Debug.Assert(memberTypes != DynamicallyAccessedMemberTypes.None);

            if (memberTypes == DynamicallyAccessedMemberTypes.All)
                return $"'{nameof(DynamicallyAccessedMemberTypes)}.{nameof(DynamicallyAccessedMemberTypes.All)}'";

            var memberTypesList = AllDynamicallyAccessedMemberTypes
                .Where(damt => (memberTypes & damt) == damt && damt != DynamicallyAccessedMemberTypes.None)
                .ToList();

            if (memberTypes.HasFlag(DynamicallyAccessedMemberTypes.PublicConstructors))
                memberTypesList.Remove(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor);

            return string.Join(", ", memberTypesList.Select(mt =>
            {
                string mtName = mt == DynamicallyAccessedMemberTypes.Interfaces
                    ? nameof(DynamicallyAccessedMemberTypes.Interfaces)
                    : mt.ToString();

                return $"'{nameof(DynamicallyAccessedMemberTypes)}.{mtName}'";
            }));
        }

        private static readonly DynamicallyAccessedMemberTypes[] AllDynamicallyAccessedMemberTypes = GetAllDynamicallyAccessedMemberTypes();

        private static DynamicallyAccessedMemberTypes[] GetAllDynamicallyAccessedMemberTypes()
        {
            var values = new HashSet<DynamicallyAccessedMemberTypes>(
                                Enum.GetValues(typeof(DynamicallyAccessedMemberTypes))
                                .Cast<DynamicallyAccessedMemberTypes>());
            values.Add(DynamicallyAccessedMemberTypes.Interfaces);
            return values.ToArray();
        }

        public static (DiagnosticId Id, string[] Arguments) GetDiagnosticForAnnotationMismatch(ValueWithDynamicallyAccessedMembers source, ValueWithDynamicallyAccessedMembers target, string missingAnnotations)
        {
            source = source switch
            {
                // FieldValue and MethodReturnValue have only one diagnostic argument, so formatting throws when the source
                // is a NullableValueWithDynamicallyAccessedMembers.
                // The correct behavior here is to unwrap always, as the underlying type is the one that has the annotations,
                // but it is a breaking change for other UnderlyingTypeValues.
                // https://github.com/dotnet/runtime/issues/93800
                NullableValueWithDynamicallyAccessedMembers { UnderlyingTypeValue: FieldValue or MethodReturnValue } nullable => nullable.UnderlyingTypeValue,
                _ => source
            };
            DiagnosticId diagnosticId = (source, target) switch
            {
                (MethodParameterValue maybeThisSource, MethodParameterValue maybeThisTarget) when maybeThisSource.IsThisParameter() && maybeThisTarget.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchThisParameterTargetsThisParameter,
                (MethodParameterValue maybeThis, MethodParameterValue) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchThisParameterTargetsParameter,
                (MethodParameterValue maybeThis, MethodReturnValue) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchThisParameterTargetsMethodReturnType,
                (MethodParameterValue maybeThis, FieldValue) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchThisParameterTargetsField,
                (MethodParameterValue maybeThis, GenericParameterValue) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchThisParameterTargetsGenericParameter,
                (MethodParameterValue, MethodParameterValue maybeThis) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchParameterTargetsThisParameter,
                (MethodParameterValue, MethodParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchParameterTargetsParameter,
                (MethodParameterValue, MethodReturnValue) => DiagnosticId.DynamicallyAccessedMembersMismatchParameterTargetsMethodReturnType,
                (MethodParameterValue, FieldValue) => DiagnosticId.DynamicallyAccessedMembersMismatchParameterTargetsField,
                (MethodParameterValue, GenericParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchParameterTargetsGenericParameter,
                (MethodReturnValue, MethodParameterValue maybeThis) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchMethodReturnTypeTargetsThisParameter,
                (MethodReturnValue, MethodParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchMethodReturnTypeTargetsParameter,
                (MethodReturnValue, MethodReturnValue) => DiagnosticId.DynamicallyAccessedMembersMismatchMethodReturnTypeTargetsMethodReturnType,
                (MethodReturnValue, FieldValue) => DiagnosticId.DynamicallyAccessedMembersMismatchMethodReturnTypeTargetsField,
                (MethodReturnValue, GenericParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchMethodReturnTypeTargetsGenericParameter,
                (FieldValue, MethodParameterValue maybeThis) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchFieldTargetsThisParameter,
                (FieldValue, MethodParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchFieldTargetsParameter,
                (FieldValue, MethodReturnValue) => DiagnosticId.DynamicallyAccessedMembersMismatchFieldTargetsMethodReturnType,
                (FieldValue, FieldValue) => DiagnosticId.DynamicallyAccessedMembersMismatchFieldTargetsField,
                (FieldValue, GenericParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchFieldTargetsGenericParameter,
                (GenericParameterValue, MethodParameterValue maybeThis) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsThisParameter,
                (GenericParameterValue, MethodParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsParameter,
                (GenericParameterValue, MethodReturnValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsMethodReturnType,
                (GenericParameterValue, FieldValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsField,
                (GenericParameterValue, GenericParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsGenericParameter,
                (NullableValueWithDynamicallyAccessedMembers, MethodParameterValue maybeThis) when maybeThis.IsThisParameter() => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsThisParameter,
                (NullableValueWithDynamicallyAccessedMembers, MethodParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsParameter,
                (NullableValueWithDynamicallyAccessedMembers, MethodReturnValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsMethodReturnType,
                (NullableValueWithDynamicallyAccessedMembers, FieldValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsField,
                (NullableValueWithDynamicallyAccessedMembers, GenericParameterValue) => DiagnosticId.DynamicallyAccessedMembersMismatchTypeArgumentTargetsGenericParameter,

                _ => throw new NotImplementedException($"Unsupported source context {source} or target context {target}.")
            };

            var args = new List<string>();
            args.AddRange(target.GetDiagnosticArgumentsForAnnotationMismatch());
            args.AddRange(source.GetDiagnosticArgumentsForAnnotationMismatch());
            args.Add(missingAnnotations);
            return (diagnosticId, args.ToArray());
        }
    }
}
